昨日一層一層瀏覽了不同層次的抽象層,最後把寫入檔案的動作看完,整個 Hello World 程式也暫告結束。
過去幾天我們一步一步的看過 Hello World 主程式所使用的 GO 語言函式庫的內容。任何一個對於程式語言有基礎觀念、並且對於類 Unix 作業系統稍有接觸的的讀者都能夠預期到這樣一個初學者程式應該要有的功能,那就是在呼叫 write()
系統呼叫之前,將傳入的格式化字串整併起來,然後呼叫,至少理論上是如此。
但是顯然,GO 語言給了我們更多驚喜!看看我們前幾日總共累積了多少懸而未決的疑問:
netpoll
系統是指什麼?顯然在創建檔案的時候很重要。runtime.SetFinalizer
是什麼?在整個 GO 語言 runtime 中扮演何種角色?arg.(type)
這種功能被稱作 reflect。GO 語言的 reflect 是怎麼做的?internal/race
是怎麼樣的函式庫?功能?sync
是怎樣的函式庫?功能?runtime.KeepAlive
大致上可以顧名思義。但為什麼它出現在讀寫之後?讀寫之前難道就沒有被 runtime 影響的危險嗎?sigpipe
,GO 語言如何處理 signal?也如筆者第一日預告的那般,這些都是大哉問。今天既然是前一個主題完結的時刻,我們就稍事休息,決定一下下一個主題的走向;至於如何決定,當然就是從這些未釐清的疑點中找尋靈感了。
其實從寫下第一行程式碼,到接觸整個資訊世界,rumtime 這個詞就絲毫不教人陌生,畢竟這是許多動態語言都有的所謂執行期環境的特徵,但是具體來說那到底是什麼?始終像是背景設定的一部分一樣,看不見摸不著。這個系列力求深入 GO 語言的實作,正好有機會可以一探究竟。
我們第一個遭遇到的是 runtime.SetFinalizer
,這個感覺上與 GO 語言本身的 defer
功能是否有什麼關係呢?如果說真的可以為資源設定一個終結者,那麼那些東西該什麼時候執行?這是否是垃圾回收機制的一部分?又,我們後來也有發現搶佔這個關鍵詞,且還有 KeepAlive
這種保險一般的函式,是否表示 GO 語言在執行的時候其實也有類似 context switch 的機制讓垃圾回收之類的函式搶先做事?如果是的話,這些功能又會如何被整合在一個 GO 語言程式中呢?可是明明是一個不需要多執行緒的程式,為什麼會有多個工作單元可以執行?乍看之下滿滿的都是問號,但是筆者相信有時候問問題比答案更重要,尤其是答案離我們還有一段距離的時候,透過以往從作業系統、程式語言等地方獲得的知識,大概能夠引導問題的方向。
當然,這個部份讓人充滿了求知慾想要一探究竟;但是筆者決定將 runtime
、race
、sync
等函式庫的探索先擺到後面,因為總有一股直覺,這些東西應該和 goroutine 以及 channal 一起看會更有效果。更何況,現在對筆者來說缺乏一個好的切入點來觀察這些功能,所以先行擱置。
我們一開始關注 os.Stdout
的時候就已經發現一般檔案與網路串流雖然共用檔案描述子這個介面,但是在抽象層的角度來講很早就已經分家了。所以雖然我們對於這個資料結構的認識仍屬模糊,但只要交叉比對這兩種不同的使用情境,比方說寫入檔案對比傳出封包,應該就可以看到其中的差異,尤其是非阻塞型檔案與輪詢的關係。
這個部份比前段的非同步項目稍微好一點,因為我們很明顯可以多作幾個範例程式就可以開始比對。但是筆者也想先將這個部份擱置下來,因為有另外一項筆者很感興趣的部份。
是的,其實這就是筆者最感興趣的部份。
所以我們將再度回頭看看這支 Hello World 程式,但不一樣的是這次我們心中不懷抱著對於列印一行字串的期待;反過來的是,我們應該要特別留意那些分明不是在 main
函式裡面的東西。從頭開始,直到結束。
頭是多前面?結束又是多後面?從程式碼到程式當然牽涉到 GO 語言的編譯與連結的實作,但是那些部份完全是獨立的元件,可以日後討論。
先來一些簡單的數據對照好了。筆者準備了另外一個 C 語言的 Hello World 程式:
#include<stdio.h>
int main(){
printf("Hello World!\n");
return 0;
}
透過靜態編譯之後的檔案與 GO 語言比較的話:C 的這個靜態執行檔是 743K,而 GO 的是 2M。
使用 readelf -a
工具觀察這兩個執行檔,發現 C 的產物定義了 1870 個 symbol,而 GO 的是 3072 個。
使用 objdump -d hw | wc -l
單純觀察程式碼部份的行數,C 是 125997 行,而 GO 是 136940 行。
這就有趣了!仔細回頭看看 readelf
的結果,發現兩者 .text
的量級差不多;拉開最多差距的則是 .rodata
,相差超過一個數量級,此外 GO 的執行檔還富含諸多除錯資訊。又,GO 語言獨有的一個區段 .gopclntab
,不知道是什麼的 table,竟然接近程式碼區段的二分之一大小,也是相當可觀。
其實也沒有特定目的,就是累積一些感覺。其實使用這些工具的重點是要找出 GO 語言的切入點。一個執行檔總是要有進入點的,這樣作業系統檢驗完執行檔與他需要的函式庫之後要轉交 CPU 給它的時候才知道如何轉交。這個進入點資訊可以使用 readelf -h
,也就是只看 ELF 檔頭就可以了。
$ readelf -h hw
ELF 檔頭:
魔術位元組:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
資料: 2 的補數,小尾序(little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: EXEC (可執行檔案)
系統架構: Advanced Micro Devices X86-64
版本: 0x1
進入點位址: 0x44f4d0
程式標頭起點: 64 (檔案內之位元組)
區段標頭起點: 456 (檔案內之位元組)
旗標: 0x0
此標頭的大小: 64 (位元組)
程式標頭大小: 56 (位元組)
Number of program headers: 7
區段標頭大小: 64 (位元組)
區段標頭數量: 23
字串表索引區段標頭: 3
得到它了!0x44f4d0
!從 objdump -d
去撈撈看這個位址是誰:
000000000044f4d0 <_rt0_amd64_linux>:
44f4d0: e9 2b c9 ff ff jmpq 44be00 <_rt0_amd64>
結果是一個名為 _rt0_amd64_linux
的函式,也沒有作什麼作業系統特別需要的前置操作,就直接跳轉到 _rt0_amd64
去,
000000000044be00 <_rt0_amd64>:
44be00: 48 8b 3c 24 mov (%rsp),%rdi
44be04: 48 8d 74 24 08 lea 0x8(%rsp),%rsi
44be09: e9 02 00 00 00 jmpq 44be10 <runtime.rt0_go>
由於筆者很久沒有摸 x86_64 了,這裡只大概記得 %rdi 和 %rsi 大概是很前面順位的參數用暫存器,因此猜測這兩個儲存到 stack 裡面的參數是 argc
和 argv
應該沒錯。接下來再跳轉到 runtime.rt0_go
去。
筆者搜尋了很久這個 rt0_go
所在的位置,還是沒有頭緒,今日就此打住吧!
.gopclntab
區段是什麼?進入點開始當然免不了有一些比較枯燥的平台相依程式碼,但是我們明天也會繼續追蹤!感謝各位讀者陪伴筆者撞牆,無論如何,理解未知的過程總是有趣的。各位讀者,我們明天再會!